package com.hrsaas.ext.spider.core.url;

import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

import com.hrsaas.ext.spider.util.Assert;

/**
 * <p>
 * 功能：url构建工具
 * <p>
 * 
 * @author wangxiaoliang@kanzhun.com
 *
 *         2018年8月1日
 */
public final class URLBuilder {

	static final String PATH_ELEMENT_DELIMITER = "/";
	static final char PORT_DELIMITER = ':';
	static final String PROTOCOL_DELIMITER = "://";
	static final char UNENCODABLE_CHARACTER = '?';
	static final char URL_ESCAPE_PREFIX = '%';
	static final char QUERY_PREFIX = '?';
	private Protocol protocol;
	private String host;
	private int port = -1;
	private String path;
	private Parameters query;
	private List<ParametersElement> queryElements;

	static final Charset ISO_LATIN = Charset.forName("ISO-8859-1");
	private ParametersDelimiter delimiter;

	static CharsetEncoder isoEncoder() {
		return ISO_LATIN.newEncoder();
	}

	public URLBuilder(Protocol protocol) {
		this.protocol = protocol;
	}

	public URLBuilder() {
		this(Protocol.HTTP);
	}

	public URLBuilder(String url) {
		this(URLParser.getURL(url));
	}
	
	public URLBuilder(URL prototype) {
		protocol = prototype.getProtocol();
		host = prototype.getHost();
		port = prototype.getPort();
		query = prototype.getParameters();
		if ( query != null) {
			Parameters p = prototype.getParameters();
			if (p instanceof ParsedParameters) {
				for (ParametersElement qe : ((ParsedParameters) p).getElements()) {
					addQueryPair(qe);
				}
			} else {
				this.query = p;
			}
		}
		path= prototype.getPath();
		
	}

	public URLBuilder addQueryPairIfNotNullOrEmpty(String key, String value) {
		if (value != null && !value.isEmpty()) {
			addQueryPair(key, value);
		}
		return this;
	}

	public URLBuilder addQueryPair(String key, String value) {
		Assert.notNull("value", value);
		Assert.notNull("key", key);
		if (query != null) {
			throw new IllegalStateException("Query explictly set");
		}
		if (queryElements == null) {
			queryElements = new LinkedList<>();
		}
		queryElements.add(new ParametersElement(key, value));
		return this;
	}

	public URLBuilder addQueryPairs(Map<String, Object> pairs, Function<Object, String> toString) {
		pairs.entrySet().forEach((e) -> {
			String s = toString.apply(e.getValue());
			if (s != null) {
				addQueryPair(e.getKey(), s);
			}
		});
		return this;
	}

	public URLBuilder addQueryPairs(Map<String, String> pairs) {
		pairs.entrySet().forEach((e) -> {
			String s = e.getValue();
			if (s != null) {
				addQueryPair(e.getKey(), s);
			}
		});
		return this;
	}

	public URLBuilder addQueryPair(ParametersElement element) {
		Assert.notNull("element", element);
		if (queryElements == null) {
			queryElements = new LinkedList<>();
		}
		queryElements.add(element);
		return this;
	}

	public URLBuilder setQueryDelimiter(ParametersDelimiter delimiter) {
		Assert.notNull("delimiter", delimiter);
		this.delimiter = delimiter;
		return this;
	}

	public URLBuilder setProtocol(String protocol) {
		Assert.notNull("protocol", protocol);
		Protocol p = Protocol.forName(protocol);
		return setProtocol(p);
	}

	public URLBuilder setProtocol(Protocol protocol) {
		Assert.notNull("protocol", protocol);
		this.protocol = protocol;
		return this;
	}

	public URLBuilder setPath(String path) {
		Assert.notNull("path", path);
		this.path = path.isEmpty() ? "/" : path;
		return this;
	}

	public URLBuilder setPort(int port) {
		Assert.nonNegative("port", port);
		this.port = port;
		return this;
	}

	public URLBuilder setQuery(Parameters query) {
		Assert.notNull("query", query);
		if (queryElements != null && !queryElements.isEmpty()) {
			throw new IllegalStateException("Query elements set");
		}
		this.query = query;
		return this;
	}

	public URLBuilder setHost(String host) {
		Assert.notNull("host", host);
		this.host = host;
		return this;
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append(protocol);
		sb.append(PROTOCOL_DELIMITER);
		sb.append(host);
		if (port != -1 && (protocol == null || port != protocol.getDefaultPort())) {
			sb.append(PORT_DELIMITER);
			sb.append(port);
		}
		sb.append(PATH_ELEMENT_DELIMITER);
		if (path != null) {
			sb.append(path);
		}
		if (queryElements != null && !queryElements.isEmpty()) {
			ParsedParameters q = new ParsedParameters(
					queryElements.toArray(new ParametersElement[queryElements.size()]));
			sb.append(delimiter == null ? q.toString(delimiter) : q);
		}
		return sb.toString();
	}

	static String escape(String toEscape, char... skip) {
		Assert.notNull("toEscape", toEscape);
		StringBuilder sb = new StringBuilder(toEscape.length() * 2);
		append(sb, toEscape);
		return sb.toString();
	}

	static void append(StringBuilder sb, String toEscape, char... skip) {
		Assert.notNull("toEscape", toEscape);
		Assert.notNull("sb", sb);
		if (skip.length > 0) {
			Arrays.sort(skip);
		}
		int max = toEscape.length();
		for (int i = 0; i < max; i++) {
			char c = toEscape.charAt(i);
			if (skip.length > 0 && Arrays.binarySearch(skip, c) >= 0) {
				sb.append(c);
				continue;
			}
			if (!isoEncoder().canEncode(c)) {
				sb.append(UNENCODABLE_CHARACTER);
				continue;
			}
			if (isLetter(c) || isNumber(c) || c == '.') {
				sb.append(c);
				continue;
			}
			appendEscaped(c, sb);
		}
	}

	static boolean isEncodableInLatin1(char c) {
		return isoEncoder().canEncode(c);
	}

	static boolean isEncodableInLatin1(CharSequence seq) {
		return isoEncoder().canEncode(seq);
	}

	static boolean isLetter(char c) {
		return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
	}

	static boolean isNumber(char c) {
		return c >= '0' && c <= '9';
	}

	static boolean isValidHostCharacter(char c) {
		return isLetter(c) || isNumber(c);
	}

	static boolean isReserved(char c) {
		switch (c) {
		case '$':
		case '&':
		case '+':
		case ',':
		case '/':
		case ':':
		case ';':
		case '=':
		case '?':
		case '@':
			return true;
		default:
			return false;
		}
	}

	static void appendEscaped(char c, StringBuilder to) {
		Assert.notNull("to", to);
		if (c == 0) {
			throw new IllegalArgumentException("Will not append the character 0");
		}
		String hex = Integer.toHexString(c);
		to.append(URL_ESCAPE_PREFIX);
		if (hex.length() == 1) {
			to.append('0');
		}
		to.append(hex);
	}

	static String unescape(String seq) {
		Assert.notNull("seq", seq);
		if (seq.indexOf('%') < 0) {
			return seq;
		}
		char[] chars = seq.toCharArray();
		StringBuilder sb = new StringBuilder(seq.length());
		for (int i = 0; i < chars.length; i++) {
			char c = chars[i];
			if (c == URL_ESCAPE_PREFIX && i < chars.length - 2 && isHexCharacter(chars[i + 1])
					&& isHexCharacter(chars[i + 2])) {
				String hex = new String(chars, i + 1, 2);
				int codePoint = Integer.valueOf(hex, 16);
				c = (char) codePoint;
				sb.append(c);
				i += 2;
				continue;
			} else {
				sb.append(c);
			}
		}
		return sb.toString();
	}

	static boolean isHexCharacter(char c) {
		switch (c) {
		case 'a':
		case 'b':
		case 'c':
		case 'd':
		case 'e':
		case 'f':
		case 'A':
		case 'B':
		case 'C':
		case 'D':
		case 'E':
		case 'F':
		case '0':
		case '1':
		case '2':
		case '3':
		case '4':
		case '5':
		case '6':
		case '7':
		case '8':
		case '9':
			return true;
		default:
			return false;
		}
	}

	public URL create() {
		return new URL(protocol, host, getPort(), path, getQuery());
	}

	int getPort() {
		if (port != -1) {
			return port;
		} else if (protocol != null) {
			return protocol.getDefaultPort();
		}
		return -1;
	}

	Parameters getQuery() {
		if (query != null) {
			return query;
		}
		if (queryElements != null && !queryElements.isEmpty()) {
			return new ParsedParameters(queryElements.toArray(new ParametersElement[queryElements.size()]));
		}
		return null;
	}

}
